package cn.com.pc.content.service;

import cn.com.pc.content.domain.Biz;
import cn.com.pc.content.domain.Site;
import cn.com.pc.content.domain.audit.AuditStatus;
import cn.com.pc.content.domain.content.*;
import cn.com.pc.content.event.ContentEvent;
import cn.com.pc.content.domain.metadata.ContentDataType;
import cn.com.pc.content.domain.metadata.ContentProperty;
import cn.com.pc.content.domain.risk.RiskEvaluator;
import cn.com.pc.content.domain.view.*;
import cn.com.pc.content.dto.*;
import cn.com.pc.content.service.es.ESQueryService;
import cn.com.pc.content.util.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.ListUtils;
import org.apache.commons.collections.SetUtils;
import org.bson.types.ObjectId;
import org.elasticsearch.common.lucene.search.function.FunctionScoreQuery;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.ConstantScoreQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import org.elasticsearch.script.Script;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.ScriptSortBuilder;
import org.elasticsearch.search.sort.SortBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ResponseStatusException;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;


/**
 * 注意：
 * 该Service利用多态进行通用处理
 * 查询表单需要继承ContentDTO类
 * 内容实体需要继承Content类
 * 如果内容类型中有特有的业务逻辑需要另外扩展
 */

@Slf4j
@Service
@RequiredArgsConstructor
public class ContentService {

    private final ContentRepository contentRepository;

    private final ContentDraftRepository contentDraftRepository;

    private final MetadataService metadataService;

    private final TagService tagService;

    private final TagRelationService tagRelationService;

    private final TagHistoryService tagHistoryRepository;

    private final ESQueryService esQueryService;

    private final ContentViewPropertyRepository contentViewPropertyRepository;

    private final MongoTemplate mongoTemplate;

    private final RiskEvaluator riskEvaluator;


    /**
     * 动态修改集合名（高并发下有问题，批量修改请用CustomMongoRepository）
     *
     * @param contentType
     */
    public void setMongodbCollection(ContentType contentType) {
        contentRepository.setCollectionName(ContentUtils.getMongoCollection(contentType));
    }


    /**
     * 查询内容列表
     *
     * @param contentBaseForm
     * @param sort
     * @return
     */
    public PageResult<?> listContentByForm(ContentBaseForm contentBaseForm, Sort sort, ContentType contentType) {
        // 过滤
        BoolQueryBuilder boolQueryBuilder = contentQueryFilter(contentBaseForm, contentType);
        // 排序
        SortBuilder<?> sortBuilder = contentSortBuilder(sort);
        // 分页
        SearchSourceBuilder searchSourceBuilder = ElasticUtil.initSearchSourceBuilder(boolQueryBuilder, sortBuilder, (contentBaseForm.getPageNum() - 1) * contentBaseForm.getPageSize(), contentBaseForm.getPageSize(), 60, true);
        // 执行搜索
        return esQueryService.search(searchSourceBuilder, contentType.getEntityDTOClass(), contentBaseForm.getPageNum(), contentBaseForm.getPageSize(), ContentUtils.getIndexName(contentBaseForm.getSite(), contentType));
    }


    /**
     * 内容搜索过滤
     *
     * @param contentBaseForm
     * @return
     */
    public BoolQueryBuilder contentQueryFilter(ContentBaseForm contentBaseForm, ContentType contentType) {
        BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery().mustNot(QueryBuilders.matchQuery("auditStatus", AuditStatus.Deleted.name()));
        if (!CollectionUtils.isEmpty(contentBaseForm.getIds())) {
            queryBuilder = queryBuilder.must(QueryBuilders.termsQuery("id", contentBaseForm.getIds()));
        }
        if (contentBaseForm.getSite() != null) {
            queryBuilder = queryBuilder.must(QueryBuilders.termQuery("site", contentBaseForm.getSite()));
        }
        if (contentBaseForm.getBiz() != null) {
            queryBuilder = queryBuilder.must(QueryBuilders.termQuery("biz", contentBaseForm.getBiz()));
        }
        if (contentBaseForm.getApp() != null) {
            queryBuilder = queryBuilder.must(QueryBuilders.termQuery("app", contentBaseForm.getApp().name()));
        }
        if (!CollectionUtils.isEmpty(contentBaseForm.getReferIds())) {
            queryBuilder = queryBuilder.must(QueryBuilders.termsQuery("referId", contentBaseForm.getReferIds()));
        }
        if (StringUtils.hasLength(contentBaseForm.getTitle())) {
            queryBuilder = queryBuilder.must(QueryBuilders.multiMatchQuery(contentBaseForm.getTitle(), "title", "title.iks", "title.spy", "title.fpy"));
        }
        if (StringUtils.hasLength(contentBaseForm.getKeyword())) {
            queryBuilder = queryBuilder.must(QueryBuilders.matchQuery("keywords", contentBaseForm.getKeyword()));
        }
        if (contentBaseForm.getAuthorId() != null) {
            queryBuilder = queryBuilder.must(QueryBuilders.matchQuery("author.passportId", contentBaseForm.getAuthorId()));
        }
        if (StringUtils.hasLength(contentBaseForm.getAuthorName())) {
            queryBuilder = queryBuilder.must(QueryBuilders.matchQuery("author.fullName", contentBaseForm.getAuthorName()));
        }
        if (contentBaseForm.getAuditStatus() != null) {
            queryBuilder = queryBuilder.must(QueryBuilders.termQuery("auditStatus", contentBaseForm.getAuditStatus().name()));
        }
        if (contentBaseForm.getIsTagged() != null) {
            if (contentBaseForm.getIsTagged()) {
                queryBuilder = queryBuilder.must(QueryBuilders.existsQuery("tagIds"));
            } else {
                queryBuilder = queryBuilder.mustNot(QueryBuilders.existsQuery("tagIds"));
            }
        }
        if (StringUtils.hasLength(contentBaseForm.getTagName())) {
            queryBuilder = queryBuilder.must(QueryBuilders.matchQuery("tagNames", contentBaseForm.getTagName()));
        }
        if (StringUtils.hasLength(contentBaseForm.getNotTagName())) {
            queryBuilder = queryBuilder.mustNot(QueryBuilders.matchQuery("tagNames", contentBaseForm.getNotTagName()));
        }
        if (!CollectionUtils.isEmpty(contentBaseForm.getTagIds())) {
            queryBuilder = queryBuilder.must(QueryBuilders.matchQuery("tagIds", contentBaseForm.getTagIds().stream().map(String::valueOf).collect(Collectors.joining(" "))));
        }
        if (contentBaseForm.getMinTaggedAt() != null) {
            queryBuilder = queryBuilder.must(QueryBuilders.rangeQuery("taggedAt").gte(LocalDateUtils.format(contentBaseForm.getMinTaggedAt())));
        }
        if (contentBaseForm.getMaxTaggedAt() != null) {
            queryBuilder = queryBuilder.must(QueryBuilders.rangeQuery("taggedAt").lte(LocalDateUtils.format(contentBaseForm.getMaxTaggedAt())));
        }
        if (StringUtils.hasLength(contentBaseForm.getTaggedBy())) {
            queryBuilder = queryBuilder.must(QueryBuilders.matchQuery("taggedBy", contentBaseForm.getTaggedBy()));
        }
        if (contentBaseForm.getOnShelve() != null) {
            queryBuilder = queryBuilder.must(QueryBuilders.termQuery("onShelve", contentBaseForm.getOnShelve()));
        }
        if (!CollectionUtils.isEmpty(contentBaseForm.getMetaData())) {
            for (Map.Entry<String, Object> entry : contentBaseForm.getMetaData().entrySet()) {
                /*
                // 元数据校验
                ContentProperty contentProperty = metadataService.getContentProperty(entry.getKey(), contentBaseForm.getSite(), contentBaseForm.getBiz(), contentType);
                if (contentProperty == null) {
                    throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "请先配置元数据：" + entry.getKey());
                }
                */
                if (entry.getValue() != null) {
                    String key = entry.getKey();
                    if (key.toLowerCase().startsWith("min")) {
                        key = key.replaceFirst("min", "");
                        key = Character.toLowerCase(key.charAt(0)) + key.substring(1);
                        queryBuilder = queryBuilder.must(QueryBuilders.rangeQuery("metaData." + key).gte(entry.getValue()));
                    } else if (key.toLowerCase().startsWith("max")) {
                        key = key.replaceFirst("max", "");
                        key = Character.toLowerCase(key.charAt(0)) + key.substring(1);
                        queryBuilder = queryBuilder.must(QueryBuilders.rangeQuery("metaData." + key).lte(entry.getValue()));
                    } else {
                        queryBuilder = queryBuilder.must(QueryBuilders.matchQuery("metaData." + key, String.valueOf(entry.getValue())));
                    }
                }
            }
        }
        return queryBuilder;
    }


    public SortBuilder<?> contentSortBuilder(Sort sort) {
        SortBuilder<?> sortBuilder;
        String sortBy = sort.getSortBy();
        if (StringUtils.hasLength(sortBy)) {
            if (sortBy.contains(",")) {
                // 多字段求和排序
                List<String> scripts = new ArrayList<>();
                for (String field : sortBy.split(",")) {
                    scripts.add(String.format("doc['%s'].size()==0?0:doc['%s'].value", field, field));
                }
                sortBuilder = SortBuilders.scriptSort(new Script(String.join("+", scripts)), ScriptSortBuilder.ScriptSortType.NUMBER);

            } else {
                // 单字段排序
                sortBuilder = SortBuilders.fieldSort(sortBy);
            }

        } else {
            // 评分排序
            sortBuilder = SortBuilders.scoreSort();
        }
        sortBuilder.order(sort.getSortOrder());
        return sortBuilder;
    }


    /**
     * 查询内容详情
     *
     * @param contentId
     * @return
     */
    public ContentDTO getContent(String contentId, Site site, Biz biz, ContentType contentType) {
        ContentDTO contentDTO = getContentDTOById(contentId, site, contentType);
        if (contentDTO == null) {
            return null;
        }
        Content content = getContentById(contentId, contentType);
        if (content != null) {
            contentDTO.setMetaData(content.getMetaData());
        }
        return contentDTO;
    }


    /**
     * 查询全类型内容详情
     *
     * @param site
     * @param biz
     * @param referId
     * @return
     */
    public ObjectNode getContentDetail(Site site, Biz biz, ContentType contentType, String referId, int limit) {
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery()
                .mustNot(QueryBuilders.matchQuery("auditStatus", AuditStatus.Deleted.name()))
                .must(QueryBuilders.termQuery("referId", referId))
                .must(QueryBuilders.termQuery("site", site))
                .must(QueryBuilders.existsQuery("tagIds"));

        if (biz != null) {
            boolQueryBuilder = boolQueryBuilder.must(QueryBuilders.termQuery("biz", biz));
        }

        String indexName;
        if (contentType != null) {
            indexName = ContentUtils.getIndexName(site, contentType);
        } else {
            indexName = ContentUtils.getIndexName(site);
        }

        SearchSourceBuilder searchSourceBuilder = ElasticUtil.initSearchSourceBuilder(boolQueryBuilder);
        List<? extends ContentDTO> contentDTOS = esQueryService.search(searchSourceBuilder, ContentType.Other.getEntityDTOClass(), indexName);
        if (contentDTOS.size() == 0) {
            return null;
        }
        ContentDTO contentDTO = contentDTOS.get(0);

        List<Object> tagParent = new ArrayList<>();
        if (limit > 0) {
            for (Long id : contentDTO.getTagIds()) {
                TagDTO tagDTO = tagService.searchTagById(id);
                if (tagDTO != null) {
                    tagParent.add(new HashMap<String, Object>() {{
                        put("tag", tagDTO);
                        put("parents", tagRelationService.getTagParents(tagDTO.getId(), limit));
                    }});
                }
            }
        }

        ObjectNode contentJsonNode = (ObjectNode) JsonUtils.obj2JsonNode(contentDTO);
        contentJsonNode.set("tagParent", JsonUtils.obj2JsonNode(tagParent));
        return contentJsonNode;
    }


    /**
     * 新增内容
     *
     * @param contentDTO
     * @return
     * @throws IOException
     */
    @SneakyThrows
    @Transactional(transactionManager = "mongoTransactionManager")
    public Content createContent(ContentDTO contentDTO, Content content, ContentType contentType) {
        // 校验objectId
        boolean hasObjectId = false;
        if (StringUtils.hasLength(contentDTO.getId())) {
            hasObjectId = true;
            if (!existsObjectId(contentDTO.getId())) {
                throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "objectId不存在");
            }
        } else {
            contentDTO.setId(new ObjectId().toString());
        }

        // todo 暂时开放接收审核状态和上下架
        //contentDTO.setAuditStatus(AuditStatus.Pending);
        //contentDTO.setOnShelve(false);

        // 校验tagIds
        List<String> tagNames = new ArrayList<>();
        String tagBy = "";
        LocalDateTime tagAt = null;
        if (!CollectionUtils.isEmpty(contentDTO.getTagIds())) {
            Map<String, Object> map = tagService.verifyTagIds(contentDTO.getTagIds(), contentDTO.getSite());
            contentDTO.setTagIds((Set<Long>) map.get("tagIds"));
            tagNames = (List<String>) map.get("tagNames");
            if (StringUtils.hasLength(contentDTO.getTaggedBy())) {
                tagBy = contentDTO.getTaggedBy();
            } else {
                tagBy = contentDTO.getCreatedBy();
            }
            if (contentDTO.getTaggedAt() != null) {
                tagAt = contentDTO.getTaggedAt();
            } else {
                tagAt = contentDTO.getCreatedAt();
            }
        }

        // 校验metaData内属性
        Map<String, Object> metaData = contentDTO.getMetaData();
        Map<String, Object> indexMetaData = new HashMap<>();
        if (!CollectionUtils.isEmpty(metaData)) {
            for (Map.Entry<String, Object> entry : metaData.entrySet()) {
                ContentProperty contentProperty = metadataService.getContentProperty(entry.getKey(), contentDTO.getSite(), contentDTO.getBiz(), contentType);
                if (contentProperty == null) {
                    throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "请先配置属性" + entry.getKey());
                }
                // 根据元数据的配置转换数据类型
                ContentDataType dataType = contentProperty.getDataType();
                Object dataValue = dataType.parse(entry.getValue());
                if (dataValue == null) {
                    throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "数据类型有误" + entry.getKey());
                }
                entry.setValue(dataValue);

                if (contentProperty.getHasIndex()) {
                    indexMetaData.put(entry.getKey(), dataValue);
                }
            }
        }

        BeanUtils.copyPropertiesIgnoreNull(contentDTO, content);
        //文章类型增加text视图转换为view存入mongodb
        content = convertView(content, null, contentType);
        // 保存mongodb
        content = contentRepository.save(content, ContentUtils.getMongoCollection(contentType));

        // 删除ContentDraft对象
        if (hasObjectId) {
            contentDraftRepository.deleteById(contentDTO.getId());
        }

        riskEvaluator.evaluate(content, contentType);

        // 保存es
        // 获取保存数据库时产生的默认值
        BeanUtils.copyProperties(content, contentDTO);
        contentDTO.setTagNames(tagNames);
        contentDTO.setTaggedBy(tagBy);
        contentDTO.setTaggedAt(tagAt);
        contentDTO.setMetaData(indexMetaData);
        SpringCtxUtils.publishEvent(new ContentEvent(contentDTO, "createIndex", Map.of("indexName", ContentUtils.getIndexName(contentDTO.getSite(), contentType), "id", content.getId())));

        return content;
    }


    /**
     * 修改内容
     *
     * @param contentDTO
     * @param content
     * @return
     */
    @SneakyThrows
    @Transactional(transactionManager = "mongoTransactionManager")
    public Content updateContent(ContentDTO contentDTO, Content content, ContentType contentType) {
        if (StringUtils.hasLength(contentDTO.getId()) && !contentDTO.getId().equals(content.getId())) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "objectId有误");
        }
        // 不可修改属性
        String ip = contentDTO.getIp() == null ? content.getIp() : contentDTO.getIp(); // kafka推送更新不修改ip
        contentDTO.setId(content.getId());
        contentDTO.setSite(content.getSite());
        contentDTO.setBiz(content.getBiz());
        contentDTO.setApp(content.getApp());
        contentDTO.setIp(ip);
        contentDTO.setReferId(content.getReferId());
        // todo 暂时开放接收审核状态和上下架
        if (contentDTO.getAuditStatus() == null) {
            contentDTO.setAuditStatus(content.getAuditStatus());
        }
        if (contentDTO.getOnShelve() == null) {
            contentDTO.setOnShelve(content.isOnShelve());
        }
        contentDTO.setCreatedAt(content.getCreatedAt());
        contentDTO.setCreatedBy(content.getCreatedBy());

        // todo 暂时开放打标签
        if (contentDTO.getTagIds() != null) {
            Map<String, Object> map = tagService.verifyTagIds(contentDTO.getTagIds(), contentDTO.getSite(), true);
            contentDTO.setTagIds((Set<Long>) map.get("tagIds"));
            contentDTO.setTagNames((List<String>) map.get("tagNames"));
            if (contentDTO.getTaggedBy() == null) {
                contentDTO.setTaggedBy(content.getUpdatedBy());
            }
            if (contentDTO.getTaggedAt() == null) {
                contentDTO.setTaggedAt(content.getUpdatedAt());
            }
        } else {
            // 不打标签, 保持不变
            contentDTO.setTagIds(content.getTagIds());
            ContentDTO contentDTOById = getContentDTOById(content.getId(), content.getSite(), contentType);
            if (contentDTOById != null && SetUtils.isEqualSet(contentDTOById.getTagIds(), content.getTagIds())) {
                contentDTO.setTagNames(contentDTOById.getTagNames());
                contentDTO.setTaggedAt(contentDTOById.getTaggedAt());
                contentDTO.setTaggedBy(contentDTOById.getTaggedBy());
            } else {
                log.warn("数据不同步，id:{}", content.getId());
                Map<String, Object> map = tagService.verifyTagIds(content.getTagIds(), content.getSite(), true);
                contentDTO.setTagIds((Set<Long>) map.get("tagIds"));
                contentDTO.setTagNames((List<String>) map.get("tagNames"));
                contentDTO.setTaggedBy(content.getUpdatedBy());
                contentDTO.setTaggedAt(content.getUpdatedAt());
            }
        }

        // 需审核属性
        boolean needAudit = false;
        if ((contentDTO.getTitle() != null && !contentDTO.getTitle().equals(content.getTitle()))
                || (contentDTO.getSummary() != null && !contentDTO.getSummary().equals(content.getSummary()))
                || (contentDTO.getKeywords() != null && !ListUtils.isEqualList(contentDTO.getKeywords(), content.getKeywords()))
                || (contentDTO.getCoverImages() != null && !ListUtils.isEqualList(contentDTO.getCoverImages(), content.getCoverImages()))
                || (contentDTO.getAuthor() != null && !contentDTO.getAuthor().equals(content.getAuthor()))) {
            needAudit = true;
        }

        // metadata内属性
        Map<String, Object> metaData = content.getMetaData();
        Map<String, Object> newMetaData = contentDTO.getMetaData();
        Map<String, Object> indexMetaData = new HashMap<>();
        if (!CollectionUtils.isEmpty(newMetaData)) {
            for (Map.Entry<String, Object> entry : newMetaData.entrySet()) {
                ContentProperty contentProperty = metadataService.getContentProperty(entry.getKey(), content.getSite(), content.getBiz(), contentType);
                if (contentProperty == null) {
                    throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "请先配置属性" + entry.getKey());
                }
                // 根据元数据的配置转换数据类型
                ContentDataType dataType = contentProperty.getDataType();
                Object dataValue = dataType.parse(entry.getValue());
                if (dataValue == null) {
                    throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "数据类型有误" + entry.getKey());
                }

                // 新增的元数据 或 已修改值的元数据 是否需要审核
                if (!metaData.containsKey(entry.getKey())
                        || !metaData.get(entry.getKey()).equals(dataValue)) {
                    if (contentProperty.getNeedAudit()) {
                        needAudit = true;
                    }
                }

                // 是否需要索引
                if (contentProperty.getHasIndex()) {
                    indexMetaData.put(entry.getKey(), dataValue);
                }
            }
        }

        // statData内属性
        Map<String, Integer> newstatData = contentDTO.getStatData();
        if (!CollectionUtils.isEmpty(newstatData)) {
            for (Map.Entry<String, Integer> entry : newstatData.entrySet()) {
                if (!StatData.getNames().contains(entry.getKey())) {
                    throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "statData参数有误");
                }
            }
        }

        //在contentDTO和content copy前，先对文章类型增加text视图转换为view存入mongodb
        content = convertView(content, contentDTO, contentType);
        if (needAudit) {
            // todo 暂时开放接收审核状态
            //contentDTO.setAuditStatus(AuditStatus.Pending);
        }
        BeanUtils.copyProperties(contentDTO, content);
        contentDTO.setMetaData(indexMetaData);

        // 保存mongodb
        content = contentRepository.save(content, ContentUtils.getMongoCollection(contentType));

        if (needAudit) {
            riskEvaluator.evaluate(content, contentType);
        }

        // 保存es
        SpringCtxUtils.publishEvent(new ContentEvent(contentDTO, "updateIndex", Map.of("indexName", ContentUtils.getIndexName(contentDTO.getSite(), contentType), "id", contentDTO.getId())));
        return content;
    }


    /**
     * 局部修改内容
     *
     * @param contentDTO
     * @param content
     * @param contentType
     * @return
     */
    @SneakyThrows
    @Transactional(transactionManager = "mongoTransactionManager")
    public Content partialUpdateContent(ContentDTO contentDTO, Content content, ContentType contentType) {
        // 不可修改属性
        String ip = contentDTO.getIp() == null ? content.getIp() : contentDTO.getIp(); // kafka推送更新不修改ip
        contentDTO.setId(content.getId());
        contentDTO.setSite(content.getSite());
        contentDTO.setBiz(content.getBiz());
        contentDTO.setApp(content.getApp());
        contentDTO.setIp(ip);
        contentDTO.setReferId(content.getReferId());
        contentDTO.setAuditStatus(content.getAuditStatus());
        contentDTO.setCreatedAt(content.getCreatedAt());
        contentDTO.setCreatedBy(content.getCreatedBy());
        contentDTO.setTagIds(content.getTagIds());
        contentDTO.setOnShelve(content.isOnShelve());

        ContentDTO contentDTOById = getContentDTOById(content.getId(), content.getSite(), contentType);
        Map<String, Object> indexMetaData = new HashMap<>();
        List<String> tagNames = new ArrayList<>();
        String tagBy = "";
        LocalDateTime tagAt = null;
        if (contentDTOById != null && SetUtils.isEqualSet(contentDTOById.getTagIds(), content.getTagIds())) {
            tagNames = contentDTOById.getTagNames();
            tagBy = contentDTOById.getTaggedBy();
            tagAt = contentDTOById.getTaggedAt();
            indexMetaData = contentDTOById.getMetaData();
        } else {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "数据不同步，id:" + content.getId());
        }

        // 需审核属性
        boolean needAudit = false;
        if ((contentDTO.getTitle() != null && !contentDTO.getTitle().equals(content.getTitle()))
                || (contentDTO.getSummary() != null && !contentDTO.getSummary().equals(content.getSummary()))
                || (contentDTO.getKeywords() != null && !ListUtils.isEqualList(contentDTO.getKeywords(), content.getKeywords()))
                || (contentDTO.getCoverImages() != null && !ListUtils.isEqualList(contentDTO.getCoverImages(), content.getCoverImages()))
                || (contentDTO.getAuthor() != null && !contentDTO.getAuthor().equals(content.getAuthor()))) {
            needAudit = true;
        }

        // metadata内属性
        Map<String, Object> metaData = content.getMetaData();
        Map<String, Object> newMetaData = contentDTO.getMetaData();
        Map<String, Object> newIndexMetaData = new HashMap<>();
        if (!CollectionUtils.isEmpty(newMetaData)) {
            for (Map.Entry<String, Object> entry : newMetaData.entrySet()) {
                ContentProperty contentProperty = metadataService.getContentProperty(entry.getKey(), content.getSite(), content.getBiz(), contentType);
                if (contentProperty == null) {
                    throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "请先配置属性" + entry.getKey());
                }
                // 根据元数据的配置转换数据类型
                ContentDataType dataType = contentProperty.getDataType();
                Object dataValue = dataType.parse(entry.getValue());
                if (dataValue == null) {
                    throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "数据类型有误" + entry.getKey());
                }

                // 新增的元数据 或 已修改值的元数据 是否需要审核
                if (!metaData.containsKey(entry.getKey())
                        || !metaData.get(entry.getKey()).equals(dataValue)) {
                    if (contentProperty.getNeedAudit()) {
                        needAudit = true;
                    }
                }

                // 是否需要索引
                if (contentProperty.getHasIndex()) {
                    newIndexMetaData.put(entry.getKey(), dataValue);
                }
            }
            // 保留原属性
            metaData.putAll(newMetaData);
            indexMetaData.putAll(newIndexMetaData);
        }
        contentDTO.setMetaData(metaData);

        // statData内属性
        Map<String, Integer> statData = content.getStatData();
        Map<String, Integer> newstatData = contentDTO.getStatData();
        if (!CollectionUtils.isEmpty(newstatData)) {
            for (Map.Entry<String, Integer> entry : newstatData.entrySet()) {
                if (!StatData.getNames().contains(entry.getKey())) {
                    throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "statData参数有误");
                }
            }
            // 保留原属性
            statData.putAll(newstatData);
        }
        contentDTO.setStatData(statData);

        //在contentDTO和content copy前，先对文章类型增加text视图转换为view存入mongodb
        content = convertView(content, contentDTO, contentType);
        if (needAudit) {
            // todo 暂时开放接收审核状态
            //contentDTO.setAuditStatus(AuditStatus.Pending);
        }
        BeanUtils.copyPropertiesIgnoreNull(contentDTO, content);

        // 保存mongodb
        content = contentRepository.save(content, ContentUtils.getMongoCollection(contentType));

        if (needAudit) {
            riskEvaluator.evaluate(content, contentType);
        }

        // 保存es
        BeanUtils.copyProperties(content, contentDTO);
        contentDTO.setTagNames(tagNames);
        contentDTO.setTaggedBy(tagBy);
        contentDTO.setTaggedAt(tagAt);
        contentDTO.setMetaData(indexMetaData);
        SpringCtxUtils.publishEvent(new ContentEvent(contentDTO, "updateIndex", Map.of("indexName", ContentUtils.getIndexName(contentDTO.getSite(), contentType), "id", contentDTO.getId())));
        return content;
    }


    /**
     * 上下架内容
     *
     * @param content
     * @return
     */
    @SneakyThrows
    @Transactional(transactionManager = "mongoTransactionManager")
    public Content shelveContent(Content content, ContentType contentType) {
        // 保存mongodb
        content = contentRepository.save(content, ContentUtils.getMongoCollection(contentType));
        // 保存es
        ObjectMapper objectMapper = new ObjectMapper();
        ObjectNode objectNode = objectMapper.createObjectNode();
        objectNode.put("onShelve", content.isOnShelve()).put("updatedBy", content.getUpdatedBy()).put("updatedAt", LocalDateUtils.format(content.getUpdatedAt()));

        SpringCtxUtils.publishEvent(new ContentEvent(objectNode, "updateIndex", Map.of("indexName", ContentUtils.getIndexName(content.getSite(), contentType), "id", content.getId())));
        return content;
    }


    /**
     * 修改内容状态
     *
     * @param content
     */
    @Transactional(transactionManager = "mongoTransactionManager")
    public void updateContentAuditStatus(Content content, ContentType contentType, AuditStatus auditStatus) {
        ContentDTO contentDTO = getContentDTOById(content.getId(), content.getSite(), contentType);
        if (contentDTO == null) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "数据不同步，id:" + content.getId());
        }

        content.setAuditStatus(auditStatus);
        contentDTO.setAuditStatus(auditStatus);
        // 保存mongodb
        contentRepository.save(content, ContentUtils.getMongoCollection(contentType));
        // 保存es
        SpringCtxUtils.publishEvent(new ContentEvent(contentDTO, "updateIndex", Map.of("indexName", ContentUtils.getIndexName(content.getSite(), contentType), "id", content.getId())));
    }

    /**
     * 删除内容
     *
     * @param content
     * @param contentType
     */
    public void deleteContent(Content content, ContentType contentType) {
        updateContentAuditStatus(content, contentType, AuditStatus.Deleted);
    }

    /**
     * 审核通过内容
     *
     * @param content
     * @param contentType
     */
    public void publishContent(Content content, ContentType contentType) {
        updateContentAuditStatus(content, contentType, AuditStatus.Publish);
    }

    /**
     * 审核不通过内容
     *
     * @param content
     * @param contentType
     */
    public void rejectContent(Content content, ContentType contentType) {
        updateContentAuditStatus(content, contentType, AuditStatus.Reject);
    }

    /**
     * 内容打标签
     *
     * @param content
     * @param contentDTO
     * @return
     */
    @SneakyThrows
    @Transactional(transactionManager = "mongoTransactionManager")
    @SuppressWarnings("unchecked")
    public Content updateContentTag(Content content, ContentDTO contentDTO, ContentType contentType) {
        // 校验tagIds
        Map<String, Object> map = tagService.verifyTagIds(contentDTO.getTagIds(), content.getSite());

        // 更新至mongodb
        content.setTagIds((Set<Long>) map.get("tagIds"));
        content = contentRepository.save(content, ContentUtils.getMongoCollection(contentType));

        // 记录打标日志
        Long tagHistoryId = tagHistoryRepository.createTagHistory(content, contentDTO, contentType);

        // 更新至es
        Map<String, Object> updateData = new HashMap<>();
        updateData.put("tagIds", map.get("tagIds"));
        updateData.put("tagNames", map.get("tagNames"));
        // todo:2021/1/25临时开放参数
        if (contentDTO.getTaggedAt() != null) {
            updateData.put("taggedAt", LocalDateUtils.format(contentDTO.getTaggedAt()));
        } else {
            updateData.put("taggedAt", LocalDateUtils.format(content.getUpdatedAt()));
        }
        if (contentDTO.getTaggedBy() != null) {
            updateData.put("taggedBy", contentDTO.getTaggedBy());
        } else if (content.getUpdatedBy() != null) {
            updateData.put("taggedBy", content.getUpdatedBy());
        }
        updateData.put("updatedAt", LocalDateUtils.format(content.getUpdatedAt()));
        updateData.put("updatedBy", content.getUpdatedBy());

        SpringCtxUtils.publishEvent(new ContentEvent(updateData, "updateIndex", Map.of("indexName", ContentUtils.getIndexName(content.getSite(), contentType), "id", content.getId())));
        return content;
    }


    /**
     * 召回内容
     *
     * @param site
     * @param tagIdAndWeight
     * @param pageNum
     * @param pageSize
     * @return
     */
    public PageResult<Object> listRecallContent(Site site, ContentType contentType, Map<Integer, Integer> tagIdAndWeight, int pageNum, int pageSize) {
        // function_score查询的query部分
        ConstantScoreQueryBuilder scoreQueryBuilder = QueryBuilders.constantScoreQuery(
                QueryBuilders.boolQuery()
                        .must(QueryBuilders.termsQuery("tagIds", tagIdAndWeight.keySet()))
                        .must(QueryBuilders.termQuery("auditStatus", "Publish")));

        // function_score查询的functions部分
        FunctionScoreQueryBuilder.FilterFunctionBuilder[] filterFunctionBuilders = new FunctionScoreQueryBuilder.FilterFunctionBuilder[tagIdAndWeight.size()];

        int i = 0;
        for (Map.Entry<Integer, Integer> entry : tagIdAndWeight.entrySet()) {
            filterFunctionBuilders[i] = new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                    QueryBuilders.termQuery("tagIds", entry.getKey()),
                    ScoreFunctionBuilders.scriptFunction(new Script("Math.log(10 * doc['statData.cmtCount'].value + doc['statData.pvTotal'].value + 2) * Math.exp(-(doc['createdAt'].value.toInstant().toEpochMilli() + 8 * 3600 * 1000 - System.currentTimeMillis()) / 1555200000)")).setWeight(entry.getValue())
            );
            i += 1;
        }

        // function_score查询
        FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(scoreQueryBuilder, filterFunctionBuilders).scoreMode(FunctionScoreQuery.ScoreMode.SUM);

        // 分页
        SearchSourceBuilder searchSourceBuilder = ElasticUtil.initSearchSourceBuilder(functionScoreQueryBuilder, (pageNum - 1) * pageSize, pageSize, 60, true);
        // 执行搜索
        return esQueryService.search(searchSourceBuilder, pageNum, pageSize, ContentUtils.getIndexName(site, contentType));
    }


    /**
     * mongodb通过id查询内容
     *
     * @param contentId
     * @param contentType
     * @return
     */
    public Content getContentById(String contentId, ContentType contentType) {
        Query query = new Query(Criteria.where("_id").is(contentId).norOperator(Criteria.where("auditStatus").is(AuditStatus.Deleted)));
        return mongoTemplate.findOne(query, Content.class, ContentUtils.getMongoCollection(contentType));
    }


    /**
     * es通过id查询内容
     *
     * @param contentId
     * @param site
     * @param contentType
     * @return
     */
    public ContentDTO getContentDTOById(String contentId, Site site, ContentType contentType) {
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery()
                .mustNot(QueryBuilders.matchQuery("auditStatus", AuditStatus.Deleted.name()))
                .must(QueryBuilders.termQuery("id", contentId))
                .must(QueryBuilders.termQuery("site", site));
        SearchSourceBuilder searchSourceBuilder = ElasticUtil.initSearchSourceBuilder(boolQueryBuilder);
        List<? extends ContentDTO> contentDTOS = esQueryService.search(searchSourceBuilder, contentType.getEntityDTOClass(), ContentUtils.getIndexName(site, contentType));
        if (contentDTOS.size() > 0) {
            return contentDTOS.get(0);
        } else {
            return null;
        }
    }


    /**
     * mongodb通过referId查询内容
     *
     * @param referId
     * @param site
     * @param biz
     * @param contentType
     * @return
     */
    public Content getContentByReferId(String referId, Site site, Biz biz, ContentType contentType) {
        Query query = new Query(Criteria.where("referId").is(referId).and("site").is(site).and("biz").is(biz).norOperator(Criteria.where("auditStatus").is(AuditStatus.Deleted)));
        return mongoTemplate.findOne(query, Content.class, ContentUtils.getMongoCollection(contentType));
    }


    /**
     * 创建/更新内容时，根据内容创建文章视图
     *
     * @param content     数据库中已有内容数据（创建内容时即为contentDTO，更新内容时为数据库中已有内容数据）
     * @param contentDTO  接口或kafka 传入的内容数据（创建内容时需要设为null）
     * @param contentType 内容类型
     * @return 加入视图的内容
     */
    public Content convertView(Content content, ContentDTO contentDTO, ContentType contentType) {
        List<ContentViewProperty> contentViewPropertyList = null;
        Map<String, String> viewData = new HashMap<String, String>();
        try {
            contentViewPropertyList = getContentViewProperty(
                    content.getSite(), content.getBiz(), contentType);

            for (ContentViewProperty contentViewProperty : contentViewPropertyList) {

                String converterClass = contentViewProperty.getConverter();
                //获取单例
                ViewConverter viewConverter = SpringCtxUtils.getBean(converterClass, ViewConverter.class);
                String view = viewConverter.convert(content, contentDTO, contentViewProperty.getViewType());
                viewData.put(contentViewProperty.getViewType(), view);
            }

            switch (contentType) {
                case Article:
                    ArticleView articleView = new ArticleView();
                    BeanUtils.copyProperties(content, articleView);
                    articleView.setViewData(viewData);
                    return articleView;
                case Post:
                    PostView postView = new PostView();
                    BeanUtils.copyProperties(content, postView);
                    postView.setViewData(viewData);
                    return postView;
                case Question:
                    CustomizedContentView customizedContentView = new CustomizedContentView();
                    BeanUtils.copyProperties(content, customizedContentView);
                    customizedContentView.setViewData(viewData);
                    return customizedContentView;
                default:
                    return content;
            }
        } catch (Exception e) {
            log.error("To View Convert error Site({}),Biz({}),contentId({})/referId({}):",
                    content.getSite(), content.getBiz(), content.getId(), content.getReferId(), e);
            return content;
        }
    }


    /**
     * 查找视图数据配置
     *
     * @param site
     * @param biz
     * @param contentType
     * @return
     */
    public List<ContentViewProperty> getContentViewProperty(Site site, Biz biz, ContentType contentType) {
        return contentViewPropertyRepository.findBySiteAndBizAndContentTypeAndDeleted(
                site, biz, contentType, false);
    }


    /**
     * 获取mongoDB产生的objectId
     *
     * @param site
     * @param biz
     * @return
     */
    public String getObjectId(Site site, Biz biz) {
        String objectId = new ObjectId().toString();
        ContentDraft contentDraft = new ContentDraft(objectId, site, biz, LocalDateTime.now());
        contentDraft = contentDraftRepository.save(contentDraft);
        return contentDraft.getObjectId();
    }


    public boolean existsObjectId(String objectId) {
        return contentDraftRepository.existsById(objectId);
    }


    /**
     * 遍历contentTypes查找内容
     * @param id
     * @param site
     * @return
     */
    public Map<String, Object> getObjectContentType(String id, Site site) {
        HashMap<String, Object> result = new HashMap<>();

        List<ContentType> contentTypes = ContentType.getContentTypes(site);
        for (ContentType contentType : contentTypes) {
            ContentDTO contentDTO = getContentDTOById(id, site, contentType);
            if (contentDTO != null) {
                result.put("contentType", contentType);
                result.put("content", contentDTO);
                break;
            }
        }
        return result;
    }
}
